All files / Bandstand/src/app/api/avatars/[id] route.ts

100% Statements 74/74
80% Branches 20/25
100% Functions 0/0
100% Lines 74/74

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 841x   1x 1x 1x 1x 1x 1x         1x 3x 3x 3x 2x 3x 1x 1x 1x   1x 1x 1x 1x 1x     1x 6x 6x 6x 5x 6x 6x 3x 6x 2x 6x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 1x 1x 2x 2x 2x 2x   1x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x  
export const dynamic = 'force-dynamic'
 
import {prisma} from '@/lib/db'
import {requireUser} from '@/lib/guard'
import {adminOver, shareABand} from '@/lib/band'
import {logActivity} from '@/lib/songs'
import {route} from '@/lib/route'
import {AVATAR_MAX_BYTES, sniffImage} from '@/lib/avatars'
 
type Ctx = {params: {id: string}}
 
/** Only to people who share a band with them. */
export const GET = route(async (_req: Request, {params}: Ctx) => {
  const {user} = await requireUser()
  if (!(await shareABand(user.id, params.id)))
    return new Response('Not Found', {status: 404})
  const a = await prisma.avatar.findUnique({where: {userId: params.id}})
  if (!a) return new Response('Not Found', {status: 404})
  return new Response(new Uint8Array(a.data), {
    headers: {
      'Content-Type': a.mime,
      // The URL carries ?v=<upload time>, so a new photo gets a new URL
      'Cache-Control': 'private, max-age=31536000, immutable',
      'X-Content-Type-Options': 'nosniff',
    },
  })
})
 
/** Your own photo; a band's admins can set its members'. Body: the bytes. */
export const PUT = route(async (req: Request, {params}: Ctx) => {
  const {user} = await requireUser()
  if (params.id !== user.id && !(await adminOver(user.id, params.id)))
    return new Response('Forbidden', {status: 403})
  const data = Buffer.from(await req.arrayBuffer())
  if (!data.length || data.length > AVATAR_MAX_BYTES)
    return new Response('Payload Too Large', {status: 413})
  const mime = sniffImage(data)
  if (!mime) return new Response('Unsupported Media Type', {status: 415})
  const target = await prisma.user.findUnique({where: {id: params.id}})
  if (!target) return new Response('Not Found', {status: 404})
  const at = new Date()
  await prisma.$transaction(async (tx) => {
    await tx.avatar.upsert({
      where: {userId: target.id},
      create: {userId: target.id, mime, data},
      update: {mime, data},
    })
    await tx.user.update({where: {id: target.id}, data: {avatarAt: at}})
    await logActivity(tx, {
      bandId: null,
      userId: user.id,
      action: 'user.avatar',
      targetType: 'user',
      targetId: target.id,
      summary:
        target.id === user.id
          ? 'updated their photo'
          : `updated the photo for ${target.displayName ?? target.name ?? 'a member'}`,
    })
  })
  return Response.json({avatarAt: at.toISOString()})
})
 
export const DELETE = route(async (_req: Request, {params}: Ctx) => {
  const {user} = await requireUser()
  if (params.id !== user.id && !(await adminOver(user.id, params.id)))
    return new Response('Forbidden', {status: 403})
  await prisma.$transaction(async (tx) => {
    const {count} = await tx.avatar.deleteMany({where: {userId: params.id}})
    if (!count) return
    await tx.user.update({where: {id: params.id}, data: {avatarAt: null}})
    await logActivity(tx, {
      bandId: null,
      userId: user.id,
      action: 'user.avatar',
      targetType: 'user',
      targetId: params.id,
      summary:
        params.id === user.id ? 'removed their photo' : 'removed a photo',
    })
  })
  return new Response(null, {status: 204})
})